5.01. Выражения и операторы
Выражения и операторы
JavaScript — язык, в котором практически всё есть выражение. Это фундаментальное свойство, определяющее синтаксис и семантику языка: от простейших вычислений до сложных императивных конструкций. Чтобы говорить о поведении кода осознанно, необходимо чётко разделять выражения (expressions) и операторы (operators), понимать их взаимосвязь, приоритеты, ассоциативность и контекст вычисления. Эта глава посвящена систематическому описанию этих понятий — от первичных элементов до сложных композиций, используемых в реальных программах.
Что такое выражение?
Выражение — это любой фрагмент кода, который вычисляется и возвращает значение. В отличие от инструкций (statements), которые описывают действия, выражения существуют ради своего результата. Многие инструкции в JavaScript могут содержать выражения, но не каждое выражение является инструкцией в строгом смысле — хотя благодаря особенностям синтаксиса JavaScript (например, отсутствию строгого разделения expression и statement в контексте блока) эта грань часто размыта.
Примеры выражений:
42— литерал, выражение, возвращающее число."привет"— строковый литерал.x + y— бинарное выражение, возвращающее сумму.f()— вызов функции, выражение, возвращающее результат её выполнения.{ a: 1 }— литерал объекта.true ? 'да' : 'нет'— условное выражение.a = b = 5— цепочка присваиваний, возвращающая значение5.
Любое выражение имеет тип и значение (которое может быть undefined, null, 0, NaN, объектом и так далее), и может быть использовано в любом контексте, где ожидается значение: в правой части присваивания, в качестве аргумента функции, в условии if, в возвращаемом значении и так далее.
Важно: в JavaScript даже блок кода в фигурных скобках { ... } может быть выражением (например, в стрелочной функции с телом-блоком), но только если он находится в подходящем контексте — например, внутри do-выражения (предложение на стадии Stage 3 на момент 2025 года) или как часть switch-выражения в будущих версиях. На текущий момент (ECMAScript 2025) блок сам по себе — инструкция, но не выражение; однако многие синтаксические конструкции, внешне похожие на инструкции, на самом деле являются выражениями.
Что такое оператор?
Оператор — это символ или ключевое слово, которое применяется к одному или нескольким операндам (выражениям) и образует новое выражение. Операторы определяют операции: арифметические, логические, побитовые, сравнения, присваивания и другие.
Классификация операторов по количеству операндов:
- Унарные — требуют один операнд (например,
typeof x,+y,!flag); - Бинарные — требуют два операнда (например,
a + b,x === y,p && q); - Тернарные — единственный в языке: условный оператор
? :, требует три операнда.
Операторы обладают приоритетом (precedence) и ассоциативностью (associativity), определяющими порядок вычисления в составных выражениях. Например, в a + b * c сначала вычисляется b * c, потому что * имеет более высокий приоритет, чем +. Если приоритеты равны (например, a - b - c), ассоциативность (в данном случае левая) определяет, что вычисление идёт слева направо: (a - b) - c.
Рассмотрим категории операторов и выражений в JavaScript последовательно, от простейших к более сложным.
Первичные выражения
Первичные выражения — это атомарные единицы, из которых строятся более сложные конструкции. Они не содержат в себе других операторов и служат отправной точкой для разбора любого выражения.
1. Литералы
Литералы — это непосредственные значения, записанные в коде. Каждый тип данных имеет свой синтаксис литералов:
- Числовые:
42,3.14,0xFF,0b1010,1e6; - Строковые:
'одинарные',"двойные",`шаблонные ${expr}`(шаблонные строки будут рассмотрены отдельно); - Логические:
true,false; nullиundefined;- Регулярные выражения:
/abc/gi; - BigInt:
123n.
Литералы всегда вычисляются в значения соответствующих типов. Стоит подчеркнуть, что строковые литералы в одинарных и двойных кавычках семантически эквивалентны — различие чисто синтаксическое (удобство экранирования). Шаблонные строки, в отличие от них, являются вызовом конструктора, и их вычисление включает интерполяцию и, при наличии тега, вызов функции-тега.
2. Идентификаторы
Имя переменной, параметра или свойства, определённое в текущей области видимости, — это выражение, возвращающее значение, связанное с этим именем. Например, x, count, isReady. Разрешение имени происходит по цепочке областей видимости (scope chain), и если имя не найдено, возникает ReferenceError (если только не используется в контексте typeof, который допускает неопределённые идентификаторы).
3. Ключевое слово this
this — особое выражение, значение которого определяется контекстом вызова, а не местом определения. В глобальном контексте (вне функций и в не-strict mode) this ссылается на глобальный объект (window в браузере, global в Node.js); в strict mode — undefined. В методах объекта this указывает на объект, через который вызван метод. В стрелочных функциях this наследуется от лексического окружения. Конструкторы и bind/call/apply также влияют на this.
4. Литералы массивов и объектов
[1, 2, 3]— массив, выражение, создающее новый объектArray;{ x: 1, y: 2 }— объект, создаётся экземплярObjectс указанными свойствами.
Эти конструкции — выражения, а не инструкции. Их можно использовать в любом месте, где ожидается значение: присвоить переменной, передать в функцию, вернуть из неё. Синтаксис допускает вычисляемые ключи ({ [key]: value }), сокращённые методы, геттеры/сеттеры — всё это входит в единый механизм инициализации объектов.
5. Литералы функций
Функции в JavaScript — объекты первого класса. Их можно определять прямо в выражениях:
- Анонимное функциональное выражение:
function (x) { return x * 2; }; - Именованное функциональное выражение:
function double(x) { return x * 2; }(имяdoubleдоступно только внутри функции); - Стрелочная функция:
x => x * 2или(x, y) => ({ sum: x + y }).
Функциональное выражение создаёт объект функции и возвращает ссылку на него. Оно может быть использовано в любом контексте, где нужен объект: присвоено переменной, передано как аргумент, возвращено из другой функции.
6. Литералы классов
Классы в ES6 — это синтаксический сахар над функциями-конструкторами, но с ключевыми отличиями в семантике (например, строгий режим по умолчанию, отсутствие подъёма). Класс-выражение:
const MyClass = class {
constructor(name) {
this.name = name;
}
greet() {
return `Привет, ${this.name}`;
}
};
Такой литерал — выражение, возвращающее конструктор класса. Он может быть анонимным (class { ... }) или именованным (class MyClass { ... }), причём имя в последнем случае также доступно только внутри тела класса.
7. Шаблонные строки
Шаблонные строки — это выражения, заключённые в обратные кавычки: `Привет, ${name}!`. При вычислении интерполируются значения, возвращаемые вложенными выражениями (${...}), и конкатенируются в итоговую строку.
Если перед шаблонной строкой стоит идентификатор (например, tag), это становится тегированной шаблонной строкой — особой формой вызова функции:
function tag(strings, ...values) {
return strings[0] + values[0].toUpperCase() + strings[1];
}
const name = "мир";
const result = tag`Привет, ${name}!`; // → "Привет, МИР!"
В этом случае tag получает массив «голых» строковых частей и список значений выражений. Это мощный инструмент для создания DSL, безопасного экранирования, локализации и других метапрограммных задач.
Левосторонние выражения (Left-Hand Side Expressions)
Эта категория объединяет выражения, которые могут стоять слева от оператора присваивания или использоваться для вызова (то есть обозначают место в памяти или свойство, доступное для записи или вызова).
1. Доступ к свойствам
- Точечная нотация:
obj.prop— компилятором трактуется какobj["prop"], но ключ должен быть валидным идентификатором; - Квадратные скобки:
obj[key]— позволяет использовать динамические или некорректные с точки зрения идентификаторов ключи (obj["123"],obj["class"],obj[someVar]).
Оба способа возвращают ссылку на свойство — «место», откуда значение берётся и куда может быть записано. Это критически важно для понимания присваивания и мутаций.
2. Вызовы
Вызов функции f(), метода obj.method(), конструктора new C() — технически это операции над левосторонними выражениями. Выражения f, obj.method, C сначала вычисляются как ссылки, затем к ним применяется операция вызова.
3. Оператор new
new Date()
new MyClass(arg)
Оператор new создаёт новый объект, устанавливает его [[Prototype]] на C.prototype, вызывает функцию C с this, указывающим на новый объект, и возвращает либо результат вызова (если это объект), либо сам объект. Это выражение возвращает ссылку на созданный экземпляр.
4. super
Ключевое слово super используется в методах классов для доступа к свойствам и вызова методов родительского класса. super.method() эквивалентно вызову Object.getPrototypeOf(this).method.call(this), но с корректной привязкой this и проверками контекста (можно использовать только внутри методов класса, не в стрелочных функциях и не в обычных функциях).
5. import.meta
Метаданные текущего модуля: import.meta.url содержит абсолютный URL модуля, import.meta.resolve() (в некоторых средах) позволяет разрешать относительные пути. Это выражение доступно только в модульном контексте и возвращает объект метаданных.
Унарные операторы
Унарные операторы применяются к одному операнду и формируют выражение, возвращающее новое значение (или, в случае некоторых, побочный эффект). Порядок применения: оператор стоит слева от операнда (префиксная форма), за исключением постфиксных ++ и --.
1. typeof
Оператор typeof возвращает строку, указывающую тип операнда на этапе выполнения. Он безопасен: не вызывает ошибок даже при обращении к необъявленным переменным (в отличие от большинства других операций).
Результаты typeof:
| Операнд | Результат | Комментарий |
|---|---|---|
undefined | "undefined" | |
null | "object" | Историческая ошибка, сохранённая для обратной совместимости. Это не объект, но результат фиксирован. |
true, false | "boolean" | |
Числа (42, 3.14, NaN) | "number" | NaN — тоже число по типу. |
123n | "bigint" | |
| Строки | "string" | |
Символы (Symbol()) | "symbol" | |
| Функции | "function" | Не отдельный тип, а подмножество объектов, но typeof делает исключение. |
| Любой другой объект | "object" | Включая массивы, даты, регулярки, обычные объекты. |
Примеры:
typeof undeclaredVariable; // "undefined" — безопасно!
typeof null; // "object"
typeof []; // "object"
typeof /regex/; // "object" (в большинстве реализаций)
typeof (() => {}); // "function"
typeof работает на уровне runtime type. Он не различает массивы и обычные объекты — для этого нужны другие средства (Array.isArray()).
2. void
Оператор void вычисляет свой операнд, отбрасывает результат и возвращает undefined. Чаще всего используется в двух контекстах:
-
Гарантированное получение
undefined, независимо от возможного переопределения глобальногоundefined(актуально в старых средах, гдеundefinedбыл изменяемым):const trulyUndefined = void 0; -
В
javascript:-ссылках, чтобы предотвратить переход или возврат значения:<a href="javascript:void(0)">Нажми</a>
Любое выражение после void будет выполнено (включая побочные эффекты), но результат всегда undefined.
3. delete
Оператор delete пытается удалить свойство объекта. Его поведение часто неправильно интерпретируется.
delete obj.prop— возвращаетtrue, если свойство успешно удалено или не существовало;false, если свойство неконфигурируемо (например, наследуется отObject.prototype, или объявлено какconfigurable: false).delete variable— не удаляет переменную, возвращаетtrueв нестрогом режиме,SyntaxErrorв strict mode.delete arr[index]— не удаляет элемент из массива, а делает ячейку «дыркой» (hole), длина массива не меняется, иarr[index]станетundefined(ноindex in arr—false).
Пример:
const obj = { a: 1, b: 2 };
Object.defineProperty(obj, 'c', { value: 3, configurable: false });
delete obj.a; // true, 'a' удалён
'a' in obj; // false
delete obj.c; // false, 'c' не удалён
'c' in obj; // true
delete не освобождает память напрямую — он лишь удаляет привязку свойства к объекту. Сборщик мусора решает, освобождать ли память.
4. Унарные + и -
-
Унарный
+преобразует операнд в число по алгоритмуToNumber. Эффективная заменаNumber(x).+ "42" // 42
+ true // 1
+ false // 0
+ null // 0
+ undefined // NaN
+ [] // 0
+ [1] // 1
+ [1,2] // NaN -
Унарный
-делает то же самое, но дополнительно меняет знак. ДляNaNиInfinityзнак меняется соответствующе (-Infinity,-0).
Особое внимание — -0. Это отдельное значение типа number, эквивалентное 0 в сравнениях (-0 === 0 → true), но различимое через Object.is(-0, 0) → false или 1 / -0 → -Infinity.
5. Логическое отрицание !
Оператор ! преобразует операнд к логическому значению (по ToBoolean) и инвертирует его. Двукратное применение (!!x) — идиома для явного приведения к boolean.
Ложные значения (falsy) в JavaScript:
false, 0, -0, 0n, "", null, undefined, NaN.
Все остальные — истинные (truthy).
!"" // true
!"hello" // false
!!{} // true
!![] // true
!!null // false
6. Побитовое НЕ ~
Применяет поразрядное дополнение: инвертирует все биты 32-битного целого представления операнда.
Поскольку ~x эквивалентно -(x + 1), часто используется в идиомах вроде:
if (~str.indexOf('подстрока')) { ... }
— потому что indexOf возвращает -1, если не найдено, а ~(-1) === 0 (ложь), иначе — ненулевое число (истина). Однако в современном коде это считается плохой практикой; предпочтительнее str.includes('...').
7. Инкремент и декремент: ++, --
Существуют в двух формах:
- Постфиксная:
x++,x--— возвращает старое значение, затем изменяет переменную. - Префиксная:
++x,--x— сначала изменяет переменную, затем возвращает новое значение.
Применяются только к ссылкам (переменным, свойствам, элементам массива), не к выражениям (++(x + y) — ошибка).
let a = 5;
let b = a++; // b = 5, a = 6
let c = ++a; // c = 7, a = 7
Важно: операторы создают побочный эффект — изменяют состояние. Их избыточное использование в сложных выражениях (arr[i++] = ++i) ведёт к нечитаемому и неопределённому поведению и строго не рекомендуется.
8. await
Оператор await может использоваться только внутри async-функции. Он приостанавливает выполнение функции до тех пор, пока промис (или thenable) не завершится, и возвращает:
- значение — если промис выполнился успешно;
- бросает исключение — если промис отклонён.
Если операнд не промис — await заворачивает его в Promise.resolve() и немедленно возвращает значение.
async function f() {
const val = await 42; // 42
const res = await fetch('/'); // ждёт ответа
return res.json();
}
await делает асинхронный код похожим на синхронный, но не блокирует поток — выполнение других задач (например, обработчиков событий) продолжается.
Арифметические операторы
Арифметические операторы выполняют математические операции над числовыми значениями. Все (кроме +) преобразуют операнды к числу через ToNumber. Оператор + имеет двойное назначение: сложение чисел и конкатенация строк — и его поведение зависит от типов операндов.
1. Сложение +
Алгоритм:
- Вычисляются оба операнда.
- Каждый преобразуется к примитиву (
ToPrimitive, предпочтительно строка, если один из операндов — строка). - Если хотя бы один операнд — строка, происходит конкатенация.
- Иначе — числовое сложение.
Примеры:
1 + 2 // 3
"1" + 2 // "12"
1 + "2" // "12"
"hello" + [] // "hello" (массив → "" → конкатенация)
[] + [] // "" (оба → "", складываются)
{} + [] // "[object Object]" — осторожно: {} интерпретируется как блок в начале выражения!
({} + []) // "[object Object]" — теперь {} — объект.
Особые случаи:
+0 + -0→+0Infinity + (-Infinity)→NaNNaN + что угодно→NaN
2. Вычитание -, умножение *, деление /, остаток %, возведение в степень **
Все эти операторы работают строго в числовом контексте: оба операнда приводятся к number (или bigint, но нельзя смешивать типы).
-
-:5 - 2→3;5 - "2"→3;"5" - "2"→3 -
*:3 * 4→12;"3" * null→0 -
/:7 / 2→3.5;1 / 0→Infinity;-1 / 0→-Infinity -
%: остаток от деления, не математический модуль. Знак результата совпадает со знаком делимого:7 % 3 // 1
-7 % 3 // -1
7 % -3 // 1
-7 % -3 // -1В отличие от
Math.abs(x) % n,%не даёт «положительный остаток» — для этого нужна собственная реализация. -
**:2 ** 3→8;2 ** -1→0.5;(-2) ** 0.5→NaN(корень из отрицательного — комплексное, не поддерживается).
Особенности чисел в JavaScript
- Все числа — 64-битные числа с плавающей точкой по стандарту IEEE 754 (тип
number), кромеBigInt. - Точность ограничена:
0.1 + 0.2 !== 0.3→true. Это следствие двоичного представления десятичных дробей. Number.MAX_SAFE_INTEGER(2⁵³ - 1) — максимальное целое, которое можно точно представить. Для больших целых —BigInt.
Операторы сравнения и равенства
Сравнения делятся на строгие и нестрогие, а также на абстрактные и семантические (например, in, instanceof).
1. Абстрактное равенство ==
Оператор == пытается привести операнды к одному типу перед сравнением. Алгоритм сложен и содержит множество специальных правил. Вот основные случаи:
null == undefined→true(единственная «честная» пара).- Число и строка: строка → число, затем числовое сравнение.
- Булево:
true→1,false→0, затем сравнение. - Объект и примитив: объект → примитив (
ToPrimitive), затем сравнение. NaN == NaN→false(всегда).
Примеры-ловушки:
0 == false // true
"" == false // true
"0" == false // true
[] == false // true ( [] → "" → 0 → false )
[0] == false // true ( [0] → "0" → 0 )
[1] == true // true ( [1] → "1" → 1 )
[1,2] == "1,2" // true
Из-за непредсказуемости и трудностей отладки рекомендуется избегать == в пользу строгого равенства.
2. Строгое равенство ===
Проверяет равенство без приведения типов:
- Если типы различаются →
false. - Для чисел:
+0 === -0→true,NaN === NaN→false. - Для строк, булевых,
null,undefined,symbol,bigint— по значению. - Для объектов — по ссылке (два объекта равны, только если это один и тот же объект в памяти).
Для сравнения NaN используется Object.is(NaN, NaN) → true, а также Object.is(+0, -0) → false.
3. Операторы неравенства: !=, !==, <, >, <=, >=
!=и!==— отрицания==и===.- Операторы
<,>и т.д. при сравнении:- Строк — лексикографически (по кодам UTF-16);
- Чисел — численно;
- Смешанных типов — приведение к числу (кроме случая, когда один из операндов — строка, а другой — объект: сначала объект → примитив, и если получилась строка — лексикография).
"2" > "10" // true (сравнение кодов: '2' > '1')
2 > "10" // false (2 > 10)
"2" > 10 // false
"12" > "2" // false — лексикография: '1' < '2'
4. Оператор in
Проверяет, существует ли собственное или унаследованное свойство с указанным именем в объекте.
"length" in [] // true (наследуется от Array.prototype)
"push" in [] // true
"toString" in {} // true (от Object.prototype)
"a" in { a: undefined } // true — важно: проверяется *существование*, а не значение
"a" in {} // false
Имя свойства всегда приводится к строке. Для проверки собственных свойств — Object.hasOwn(obj, key) (ранее Object.prototype.hasOwnProperty.call).
5. Оператор instanceof
Проверяет, входит ли прототип правого операнда в цепочку прототипов левого.
[] instanceof Array // true
[] instanceof Object // true
new Date() instanceof Date // true
Алгоритм: left instanceof right эквивалентен right[Symbol.hasInstance](left), если определён, иначе — проверке left.[[Prototype]] === right.prototype рекурсивно.
Особенности:
- Не работает между разными контекстами (например, массив из другого фрейма не будет
instanceof Arrayв текущем); - Для встроенных типов надёжнее использовать
Array.isArray(),typeofи т.д.
Логические и условные операторы
Логические операторы в JavaScript не ограничиваются булевой алгеброй: их поведение расширено до работы с любыми значениями, и они активно используются для управления потоком данных и построения безопасных выражений.
1. Логическое И: &&
Оператор && вычисляет выражения слева направо и возвращает:
- первое ложное значение (falsy), если оно встречается;
- последнее истинное значение (truthy), если все истинны.
Это короткозамкнутое вычисление (short-circuit evaluation): правый операнд вычисляется только если левый истинен.
true && "hello" // "hello"
false && expensiveFn() // false — expensiveFn() не вызывается
null && "world" // null
0 && 1 // 0
"user" && obj.name // obj.name — только если "user" истинно (всегда)
Идиомы:
- Безопасная проверка цепочки свойств (до появления optional chaining):
const name = user && user.profile && user.profile.name; - Выполнение побочного эффекта при условии:
isValid && console.log("Валидация пройдена");
Важно: && не приводит результат к boolean. Он возвращает значение одного из операндов, что делает его мощным инструментом для управления потоком данных, но требует осторожности в условиях, где ожидается строго true/false.
2. Логическое ИЛИ: ||
Аналогично, || возвращает:
- первое истинное значение;
- последнее ложное, если все ложны.
Короткозамкнутое: правый операнд вычисляется, только если левый ложен.
false || "default" // "default"
0 || 1 // 1
null || undefined // undefined
"name" || expensiveFn() // "name" — expensiveFn() не вызывается
Идиомы:
-
Задание значений по умолчанию (устаревшая практика для параметров):
function greet(name) {
name = name || "Гость"; // опасно: если name = 0 или "", будет "Гость"
return `Привет, ${name}`;
}→ Сегодня предпочтительнее параметры по умолчанию:
function greet(name = "Гость"). -
Накопление значений:
const value = config.option || env.OPTION || DEFAULT;
3. Оператор нулевого слияния: ??
Появился в ES2020 для устранения недостатков ||. Оператор ?? возвращает правый операнд только если левый — null или undefined. В отличие от ||, он игнорирует другие ложные значения (0, "", false).
0 ?? "default" // 0
"" ?? "default" // ""
false ?? "default" // false
null ?? "default" // "default"
undefined ?? "fallback" // "fallback"
Это позволяет безопасно задавать значения по умолчанию без нарушения семантики нуля или пустой строки.
Комбинирование с && и ||:
Приоритет ?? ниже, чем у && и ||, но выше, чем у =, += и т.д. Однако запрещено писать a || b ?? c без скобок — это вызовет синтаксическую ошибку, чтобы избежать неоднозначности.
Правильно:
(a || b) ?? c
a || (b ?? c)
4. Условный (тернарный) оператор: ? :
Единственный тернарный оператор: условие ? выражение1 : выражение2.
Алгоритм:
- Вычисляется условие;
- Если
ToBoolean(условие)→true, вычисляется и возвращается выражение1; - Иначе — выражение2.
Оба выражения не вычисляются заранее — действует короткое замыкание.
const status = isLoading ? "загрузка..." : data ? "готово" : "ошибка";
Ключевое отличие от if…else: тернарный оператор — выражение, а не инструкция. Он возвращает значение и может использоваться везде, где ожидается значение: в присваивании, возврате, аргументах, шаблонных строках.
return user.isAdmin ? adminView : userView;
const message = `Счёт: ${balance >= 0 ? balance : "отрицательный"}`;
Рекомендации:
- Избегайте вложенных тернарных операторов глубже одного уровня — это ухудшает читаемость.
- Не используйте для побочных эффектов (например,
cond ? f() : g()допустимо, ноcond ? x = 1 : y = 2— лучше черезif). - При длинных ветках — выносите в переменные или используйте
if.
Битовые и побитовые операторы
Битовые операторы работают с 32-битными целыми числами со знаком в дополнительном коде (two’s complement). Перед операцией операнды приводятся к ToInt32, дробная часть отбрасывается, а старшие биты — усекаются.
1. Побитовые логические операторы
| Оператор | Название | Описание |
|---|---|---|
& | И | Возвращает 1 в бите, если оба бита = 1 |
| | ИЛИ | Возвращает 1, если хотя бы один бит = 1 |
^ | Исключающее ИЛИ | Возвращает 1, если биты различны |
~ | НЕ (уже рассмотрен) | Инвертирует все биты |
Пример:
5 & 3 // 1 — 0b101 & 0b011 = 0b001
5 | 3 // 7 — 0b101 | 0b011 = 0b111
5 ^ 3 // 6 — 0b101 ^ 0b011 = 0b110
Применения:
- Маскирование флагов:
flags & PERMISSION_READ - Установка флагов:
flags |= PERMISSION_WRITE - Инверсия флагов:
flags ^= TOGGLE_MODE
2. Побитовые сдвиги
| Оператор | Название | Описание |
|---|---|---|
<< | Левый сдвиг | Сдвигает биты влево, заполняя нулями справа. Эквивалентно умножению на 2ⁿ (но с усечением). |
>> | Правый сдвиг со знаком | Сохраняет знак: заполняет слева старшим битом (для отрицательных — единицами). |
>>> | Правый сдвиг без знака | Всегда заполняет нулями слева. Результат — беззнаковое 32-битное целое (всегда ≥ 0). |
Примеры:
8 << 1 // 16
-8 >> 1 // -4
-8 >>> 1 // 2147483644 — интерпретируется как большое положительное число
1 << 31 // -2147483648 — переполнение в signed int
Важно: все сдвиги работают с n % 32, то есть x << 33 эквивалентно x << 1.
Когда использовать битовые операции?
- Работа с низкоуровневыми данными (парсинг бинарных протоколов, шейдеры в WebGL);
- Оптимизация флагов (часто в играх, эмуляторах);
- Хэширование, криптографические примитивы.
В повседневной веб-разработке они редки — и их использование требует комментариев и обоснования.
Операторы присваивания
Оператор присваивания (=) — один из самых фундаментальных. Он вычисляет правый операнд, затем записывает результат в левостороннее выражение (LHS), и возвращает присвоенное значение.
1. Простое присваивание: =
a = b = 5; // сначала b = 5 → 5, затем a = 5 → 5
Ассоциативность — правая, поэтому цепочки работают как ожидается.
2. Составные присваивания
Комбинируют операцию и присваивание:
| Оператор | Эквивалент |
|---|---|
+= | a = a + b |
-= | a = a - b |
*= | a = a * b |
/= | a = a / b |
%= | a = a % b |
**= | a = a ** b |
&= | a = a & b |
|= | a = a | b |
^= | a = a ^ b |
<<= | a = a << b |
>>= | a = a >> b |
>>>= | a = a >>> b |
Особенность: левый операнд вычисляется один раз, что важно при работе со свойствами:
obj.prop += 1;
// эквивалентно:
// let temp = obj;
// temp.prop = temp.prop + 1;
// (а не obj.prop = obj.prop + 1 — obj не вычисляется дважды)
Это предотвращает ошибки при изменении obj между обращениями.
3. Деструктурирующее присваивание
Позволяет извлекать данные из массивов и объектов в переменные по шаблону.
Деструктуризация массива:
const [first, second, ...rest] = [10, 20, 30, 40];
// first = 10, second = 20, rest = [30, 40]
const [x, , y] = [1, 2, 3]; // пропуск элемента: x = 1, y = 3
const [a = 1, b = 2] = [undefined, 3]; // a = 1 (по умолчанию), b = 3
Деструктуризация объекта:
const { name, age: years, active = true } = { name: "Тимур", age: 30 };
// name = "Тимур", years = 30, active = true
const { length } = "привет"; // length = 6 — работает с любым итерируемым/объектом
Вложенные шаблоны:
const {
user: { name, role: { title } },
settings: { theme = "light" }
} = response;
Деструктуризация в параметрах функции:
function draw({ x = 0, y = 0, color = "black" } = {}) {
// ...
}
draw({ x: 10, color: "red" });
Обмен значений без временной переменной:
[a, b] = [b, a];
Ограничения:
- Левая часть должна быть корректным шаблоном деструктуризации;
- При несовпадении структуры —
undefined(если нет значения по умолчанию); - Ошибки при попытке присвоить
undefinedилиnullв деструктуризацию объекта (например,const { x } = null→TypeError).
Деструктуризация — отдельный механизм, интегрированный в ядро языка. Она компилируется в последовательность операций доступа к свойствам и присваиваний, и её производительность сравнима с ручным извлечением.
Операторы расширения (...) и остатка
Синтаксис ... (три точки) используется в двух принципиально разных, но внешне похожих контекстах: как расширение (spread) и как остаток (rest). Их различие определяется положением в выражении.
1. Расширение (Spread)
Оператор расширения извлекает элементы из итерируемого объекта (массива, строки, Set, Map, генератора и т.д.) и «распыляет» их в месте использования. Контексты применения:
a) В литералах массивов
const a = [1, 2];
const b = [0, ...a, 3]; // [0, 1, 2, 3]
— создаёт новый массив, копируя элементы из a. Это мелкая копия (shallow copy).
b) В литералах объектов (начиная с ES2018)
const defaults = { theme: "light", lang: "ru" };
const config = { ...defaults, lang: "en" }; // { theme: "light", lang: "en" }
— копирует собственные перечисляемые свойства из defaults в новый объект. При совпадении ключей позже указанные свойства перекрывают более ранние.
Особенности объектного spread:
- Не вызывает геттеры — читает значения напрямую;
- Не копирует символы, если они не перечисляемы;
- Не копирует прототип, не вызывает конструктор — результат всегда «плоский»
Object; - При конфликтах — побеждает последнее свойство:
{ ...{a:1}, a:2 }→{a:2}.
c) В вызовах функций
Math.max(...[10, 20, 30]); // эквивалентно Math.max(10, 20, 30)
— распаковывает итерируемый объект в список аргументов. Это замена Function.prototype.apply.
d) В строках и других итерируемых
[..."hello"] // ['h','e','l','l','o']
new Set(...[1, 1, 2]) // Set {1, 2}
Важные ограничения:
- Spread работает только с итерируемыми объектами (
Symbol.iteratorдолжен быть определён).{a:1}— не итерируемый, поэтому...{a:1}в массиве вызовет ошибку (но в объекте — разрешено, т.к. объектный spread использует другой механизм —Object.assign-подобный). - Spread не работает в левой части присваивания — там требуется rest.
2. Остаток (Rest)
Оператор остатка собирает неиспользованные элементы в новый массив или новый объект. Применяется в:
a) Параметрах функции
function sum(first, ...rest) {
return rest.reduce((a, b) => a + b, first);
}
sum(1, 2, 3, 4); // 10
— rest получает все аргументы, начиная со второго, в виде массива.
b) Деструктуризации массива
const [head, ...tail] = [1, 2, 3, 4]; // head = 1, tail = [2, 3, 4]
— tail — новый массив, содержащий оставшиеся элементы.
c) Деструктуризации объекта (ES2018+)
const { name, ...meta } = { name: "Тимур", age: 30, city: "Уфа" };
// name = "Тимур", meta = { age: 30, city: "Уфа" }
— meta получает все остальные собственные перечисляемые свойства, кроме name.
Ключевые различия между spread и rest:
| Критерий | Spread (...x) | Rest (...x) |
|---|---|---|
| Контекст | Правая часть, литералы, вызовы | Левая часть, параметры, деструктуризация |
| Действие | Распаковывает | Собирает |
| Результат | Множество значений/свойств | Один массив/объект |
| Можно использовать в одном выражении? | Да, несколько раз: [...a, ...b] | Только один раз на уровень деструктуризации: [a, ...rest] — OK; [...x, ...y] — ошибка |
Производительность и семантика:
- Оба оператора создают новые структуры данных — копирование происходит при каждом использовании.
- Spread в объектах не вызывает сеттеры — присваивание идёт напрямую.
- Rest в объектах не включает не перечисляемые свойства и символы (если не перечисляемы).
Оператор запятой ,
Оператор запятой — один из самых редко используемых, но принципиально важных. Он вычисляет оба операнда слева направо и возвращает значение правого операнда.
let a, b;
a = (1, 2, 3); // a = 3
b = (console.log("A"), console.log("B"), 42); // выведет A, B; b = 42
Особенности:
- Низший приоритет среди всех операторов — ниже, чем присваивание.
- Используется в заголовках циклов
for, где допустимы три выражения, разделённые запятыми:for (let i = 0, j = 10; i < j; i++, j--) { ... } - Применяется в минифицированном коде, макросах (например, в Babel-плагинах), и при необходимости выполнить побочные эффекты в выражении (например, в
return,throw,yield).
Не путать с:
- Запятой в литералах массивов/объектов — это разделитель элементов, не оператор;
- Запятой в объявлениях переменных (
let a, b) — синтаксис объявления, не выражение.
Пример использования в return:
function f() {
let x = 0;
return (x++, x * 2); // сначала инкремент, затем возврат x*2
}
f(); // 2
Хотя синтаксис корректен, чрезмерное использование оператора запятой ухудшает читаемость. Его применение оправдано только в узких технических сценариях.
Условные конструкции: if…else, switch
До сих пор мы говорили об выражениях (x ? a : b), которые возвращают значение. Теперь перейдём к инструкциям — конструкциям управления потоком, которые не возвращают значения и не могут использоваться в правой части присваивания.
1. if…else
Классическая конструкция ветвления. Синтаксис:
if (выражение) {
// блок, выполняемый если ToBoolean(выражение) === true
} else if (выражение2) {
// ...
} else {
// по умолчанию
}
Семантические детали:
- В скобках после
ifдолжно быть выражение (любое), которое будет приведено к булеву значению. - Фигурные скобки
{ }необязательны для однострочных блоков, но их отсутствие считается антипаттерном (ошибки при добавлении строк, проблемы сelseв «висячем» виде). elseсвязывается с ближайшимif, не имеющимelse— что может привести к неочевидному поведению при неправильной расстановке скобок.
Рекомендации:
- Всегда использовать фигурные скобки;
- Избегать «висячих»
else; - Для простых случаев — предпочитать тернарный оператор или логические операторы (если нужен результат);
- Для множества условий — рассмотреть
switchили объект-карту.
2. switch
Конструкция выбора, предназначенная для сравнения одного значения с множеством возможных вариантов.
switch (выражение) {
case значение1:
// код
break;
case значение2:
// код
break;
default:
// по умолчанию
}
Как это работает:
- Вычисляется выражение (селектор);
- Последовательно сравнивается со значениями в
caseчерез строгое равенство (===); - При совпадении — начинается выполнение с этого
case; - Выполнение продолжается до первого
break,return,throwили концаswitch; - Если совпадений нет — выполняется
default(если есть).
Поведение без break — fall-through:
switch (status) {
case "pending":
log("В обработке");
case "approved": // ← выполнится, даже если status === "pending"
log("Одобрено");
break;
}
Это осознанная возможность — например, для объединения нескольких кейсов:
case "mon":
case "tue":
case "wed":
console.log("Рабочий день");
break;
Что можно использовать в case:
- Любые выражения, вычисляемые в момент разбора
switch(но не динамически при каждом проходе):const A = 1, B = 2;
switch (x) {
case A + B: // OK, вычисляется один раз при входе в switch
} - Выражения не могут быть объектами, массивами — сравнение через
===сделает их различными.
Ограничения switch:
- Не поддерживает диапазоны (
case 1..10:— нет); - Не поддерживает условия (
case x > 5:— нет); - Чувствителен к типам (
switch (1) { case "1": ... }не сработает).
Альтернативы:
- Объект-карта:
const handlers = { "mon": fn1, "tue": fn2 }; handlers[day]?.(); - Массив с индексацией;
if…else if— для сложных условий.
Приоритеты операторов
Приоритет определяет, какие операторы вычисляются первыми в выражении без скобок. Ассоциативность — порядок вычисления операторов с одинаковым приоритетом (слева направо или справа налево).
Ниже — сокращённая, но практическая таблица приоритетов (от высшего к низшему), сгруппированная по смыслу. Полная таблица содержит 21 уровень — для повседневного использования достаточно помнить ключевые группы.
| Уровень | Операторы / конструкции | Ассоциативность | Комментарий |
|---|---|---|---|
| 21 | . [] () new (без аргументов) | левая | Доступ к свойствам, вызов, создание |
| 20 | new (с аргументами), ... (spread) | правая / нет | new Foo(), ...arr |
| 19 | ++ -- (постфиксные) | левая | x++ |
| 18 | ++ -- (префиксные), + - ! ~ typeof void delete await | правая | Унарные операторы |
| 17 | ** | правая | Возведение в степень |
| 16 | * / % | левая | Арифметика |
| 15 | + - (бинарные) | левая | Сложение/вычитание (и конкатенация) |
| 14 | << >> >>> | левая | Побитовые сдвиги |
| 13 | < <= > >= in instanceof | левая | Сравнения |
| 12 | == != === !== | левая | Равенство |
| 11 | & | левая | Побитовое И |
| 10 | ^ | левая | Побитовое XOR |
| 9 | | | левая | Побитовое ИЛИ |
| 8 | && | левая | Логическое И |
| 7 | || | левая | Логическое ИЛИ |
| 6 | ?? | левая | Нулевое слияние |
| 5 | ? : | правая | Тернарный оператор |
| 4 | = += -= и все составные присваивания | правая | Присваивание |
| 3 | yield yield* | правая | Генераторы |
| 2 | , (оператор запятой) | левая | Последовательное вычисление |
| 1 | => (стрелочные функции) | правая | Только при многострочном теле |
Практические правила чтения выражений:
- Сначала — вызовы и доступы:
obj.method()[0].prop— сначалаobj.method(), затем[0], затем.prop. - Унарные — сильнее бинарных:
!a + b→(!a) + b, а не!(a + b). **— правоассоциативен:2 ** 3 ** 2→2 ** (3 ** 2)=512, а не(2 ** 3) ** 2=64.&&и||не смешиваются без скобок с??:a || b ?? c— синтаксическая ошибка (намеренно).- Тернарный — правоассоциативен:
a ? b : c ? d : e→a ? b : (c ? d : e).
Рекомендации:
- Не полагайтесь на память приоритетов — используйте скобки для явного указания порядка, особенно при смешивании разных групп (
==,&&,??,? :); - В IDE — включите подсветку расстановки скобок (например, в VS Code:
Editor: Bracket Pair Colorization); - При рефакторинге — не удаляйте «лишние» скобки без проверки семантики.